iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 14
0
Software Development

今晚我想來點 Express 佐 MVC 分層架構系列 第 14

[今晚我想來點 Express 佐 MVC 分層架構] DAY 14 - Route Module

  • 分享至 

  • xImage
  •  

建置 RouteBase

Route Module 的設計可以很多樣, 最重要的功能即定義路由 ,所以要在 constructor() 去觸發路由註冊,這就是 Route Module 的核心,我們新增一個 route.base.ts 並放在 bases 資料夾下,以下是 route.base.ts 的內容,可以看到有一個抽象方法 registerRoute(),它就是註冊路由用的:

import { Router } from 'express';

export abstract class RouteBase {

  public router = Router();

  constructor() {
    this.initial();
  }
  
  protected initial(): void {
    this.registerRoute();
  }

  protected abstract registerRoute(): void;

}

接著我們要建置一個 app.routing.ts 作為整個 Express App 的路由集束點,為什麼不是在 App 做集束呢?因為 App 本身的工作不是在定義路由上,是在做 Express App 的相關設置,這樣能夠切割的較乾淨。下方為 app.routing.ts 的內容:

import { RouteBase } from './bases/route.base';

export class AppRoute extends RouteBase {

  constructor() {
    super();
  }

  protected registerRoute(): void {
    // 設置路由
  }

};

最後就是在 App 中進行串接,所以修改一下 app.ts,將 AppRoute 建立實例,並修改 registerRoute()

private route = new AppRoute();
private registerRoute(): void {
  this.app.use('/', this.route.router);
}

設計 API 路由

在設計完 RouteBase 後,就可以來規劃我們的 API 路由了,基本上我習慣以 /api 作為 API 的根節點,但我們不去改變 app.ts 中的配置,我們另外新增一個 api.routing.ts 的路由模組,在 app.routing.ts 中使用它。下圖為模組配置關係圖:
https://ithelp.ithome.com.tw/upload/images/20200825/20119338UVjuJQk2Ln.png

? Route 為後面後提到的配置項目,暫時不用管它,只需要知道有這東西就好

下方為更改後的資料夾結構:

├── src
|   ├── index.ts
|   ├── app.ts
|   ├── app.routing.ts                 // 本篇新增
|   ├── bases
|   |   └── route.base.ts              // 本篇新增
|   ├── main                           // 本篇新增
|   |   └── api                        // 本篇新增
|   |       ├── api.routing.ts         // 本篇新增
|   |       └── todo                   // 本篇新增
|   |           └── todo.routing.ts    // 本篇新增
|   ├── environments
|   |   ├── development.env
|   |   └── production.env
|   └── validators
|       ├── index.ts
|       └── email.validator.ts
├── package.json
└── tsconfig.json

TodoRoute

從最內層的 TodoRoute 開始做起,我們在這層做一個測試用的 API,下方為 todo.routing.ts 的程式碼:

import { RouteBase } from '../../../bases/route.base';

export class TodoRoute extends RouteBase {

  constructor() {
    super();
  }

  protected registerRoute(): void {
    this.router.get('/test', (req, res, next) => res.send('todo test.'));
  }

}

ApiRoute

透過 ApiRoute 來連結 TodoRoute 的路由,下方為 api.routing.ts 的程式碼:

import { RouteBase } from '../../bases/route.base';
import { TodoRoute } from './todo/todo.routing';

export class ApiRoute extends RouteBase {

  private todoRoute = new TodoRoute();

  constructor() {
    super();
  }

  protected registerRoute(): void {
    this.router.use('/todo', this.todoRoute.router);
  }

}

AppRoute

這裡要修改 AppRoute 的配置:

import { RouteBase } from './bases/route.base';
import { ApiRoute } from './main/api/api.routing';

export class AppRoute extends RouteBase {

  private apiRoute = new ApiRoute();

  constructor() {
    super();
  }

  protected registerRoute(): void {
    this.router.use('/api', this.apiRoute.router);
  }

};

測試

在瀏覽器中輸入 http://localhost:3000/api/todo/test 會看到以下結果:
https://ithelp.ithome.com.tw/upload/images/20200825/20119338wg5meSJQbj.png

規範 Route Module

為了方便維護系統,我在這邊與大家分享我自己的規範:

只處理路由相關策略

在 Route Module 中,只做與路由相關的事情是理所當然的,所以應盡量避免不必要的業務邏輯等程式片段,下方的程式碼就不符合此定義,應該要 把中介軟體切割出去 ,這部分後面會再告訴大家要怎麼切割,所以這篇還是用這樣的方式寫測試:

protected registerRoute(): void {
  this.router.get('/test', (req, res, next) => res.send('todo test.'));
}

保持路由的整潔

在包裝成 Route Module 的時候,應該要盡量把過於重複的路徑資源切割成另一個 Route Module,以下方例子來說,同樣都是在 /users/orders 下的資源,我會額外切出 UserRouteOrderRoute,否則會看到下面的大雜燴:

protected registerRoute(): void {
  this.router.get('/', (req, res, next) => res.end());
  this.router.get('/users', (req, res, next) => res.end());
  this.router.get('/users/:id', (req, res, next) => res.end());
  this.router.post('/users', (req, res, next) => res.end());
  this.router.patch('/users/:id', (req, res, next) => res.end());
  this.router.delete('/users/:id', (req, res, next) => res.end());
  this.router.get('/orders', (req, res, next) => res.end());
  this.router.get('/orders/:id', (req, res, next) => res.end());
}

小結

Route Module 的核心功能即為定義路由相關策略,這個觀念是很重要的,以往在寫 Express 的時候,就是把路由跟其他雜七雜八的混在一起寫,最後的下場就是很難維護,所以前面有提到要將業務邏輯切分出去,那究竟要如何切分呢?切分出去的我們又如何稱呼呢?答案就是設計 Controller,下一篇將告訴大家我的 Controller 設計方法!


上一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 13 - 規劃 Express 專案
下一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 15 - Controller
系列文
今晚我想來點 Express 佐 MVC 分層架構30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
willy361
iT邦新手 5 級 ‧ 2022-08-31 14:48:57

你好,

我試著實作您的Route Module,但是執行時遇到"undefined"錯誤,我試著插log印出"this.apiRoute"此變數,確實為"undefined",請問您有遇到相同的問題嗎?

https://ithelp.ithome.com.tw/upload/images/20220831/2011955969cdrd6cTt.png

https://ithelp.ithome.com.tw/upload/images/20220831/20119559VvmctYdThm.png

HAO iT邦研究生 2 級 ‧ 2022-10-18 21:23:03 檢舉

請問有其他資訊嗎?

小山貓 iT邦新手 5 級 ‧ 2023-02-18 22:30:18 檢舉

你好,我這邊也發生了相同的狀況。

根據我研究之後發現,應該是因為 base class 與 derived class 的初始化順序的問題。根據 這篇文章,base class 的 constructor 執行完之後才會初始化 derived class 的 properties,因此當 RouteBase 執行到 registerRoute 的時候,apiRoute 變數尚未被初始化,因此就發生了上面的錯誤。我不太確定以前為什麼不會發生,也許是某次 TypeScript 更新後產生的問題。

我的解決辦法就是不在 property 初始化 apiRoute,而是直接在 registerRoute 裡面 new ApiRoute,其他類似的地方也做類似的處理,就可以正常執行了。

tim80411 iT邦新手 5 級 ‧ 2023-03-07 01:04:50 檢舉

我有遇到相同問題,也試著驗證過這件事情,看來確實是因為順序的問題,驗證的程式碼如下:

abstract class Base {
  constructor() {
    this.setData();
  }

  protected abstract setData(): void;
}

class Child extends Base {
  private prop = (() => {
    console.log('==prop==')
    return {key: 'value'};
  })()

  constructor() {
    super()
  }

  protected setData(): void {
    console.log('==setData==')
  }
}

const child = new Child();

會先後出現
==setData==
==prop==

我要留言

立即登入留言